/* -*- mode: c++; c-basic-offset:4 -*-
    crypto/createchecksumscontroller.cpp

    This file is part of Kleopatra, the KDE keymanager
    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <config-kleopatra.h>

#include "createchecksumscontroller.h"

#include <utils/input.h>
#include <utils/output.h>
#include <utils/kleo_assert.h>

#include <Libkleo/Stl_Util>
#include <Libkleo/ChecksumDefinition>
#include <Libkleo/Classify>

#include <KLocalizedString>
#include "kleopatra_debug.h"
#include <QTemporaryFile>
#include <KConfigGroup>
#include <KSharedConfig>

#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>

#include <QPointer>
#include <QFileInfo>
#include <QThread>
#include <QMutex>
#include <QProgressDialog>
#include <QDir>
#include <QProcess>

#include <gpg-error.h>

#include <deque>
#include <map>
#include <limits>
#include <functional>

using namespace Kleo;
using namespace Kleo::Crypto;

namespace
{

class ResultDialog : public QDialog
{
    Q_OBJECT
public:
    ResultDialog(const QStringList &created, const QStringList &errors, QWidget *parent = nullptr, Qt::WindowFlags f = {})
        : QDialog(parent, f),
          createdLB(created.empty()
                    ? i18nc("@info", "No checksum files have been created.")
                    : i18nc("@info", "These checksum files have been successfully created:"), this),
          createdLW(this),
          errorsLB(errors.empty()
                   ? i18nc("@info", "There were no errors.")
                   : i18nc("@info", "The following errors were encountered:"), this),
          errorsLW(this),
          buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, this),
          vlay(this)
    {
        KDAB_SET_OBJECT_NAME(createdLB);
        KDAB_SET_OBJECT_NAME(createdLW);
        KDAB_SET_OBJECT_NAME(errorsLB);
        KDAB_SET_OBJECT_NAME(errorsLW);
        KDAB_SET_OBJECT_NAME(buttonBox);
        KDAB_SET_OBJECT_NAME(vlay);

        createdLW.addItems(created);
        QRect r;
        for (int i = 0; i < created.size(); ++i) {
            r = r.united(createdLW.visualRect(createdLW.model()->index(0, i)));
        }
        createdLW.setMinimumWidth(qMin(1024, r.width() + 4 * createdLW.frameWidth()));

        errorsLW.addItems(errors);

        vlay.addWidget(&createdLB);
        vlay.addWidget(&createdLW, 1);
        vlay.addWidget(&errorsLB);
        vlay.addWidget(&errorsLW, 1);
        vlay.addWidget(&buttonBox);

        if (created.empty()) {
            createdLW.hide();
        }
        if (errors.empty()) {
            errorsLW.hide();
        }

        connect(&buttonBox, &QDialogButtonBox::accepted, this, &ResultDialog::accept);
        connect(&buttonBox, &QDialogButtonBox::rejected, this, &ResultDialog::reject);
        readConfig();
    }
    ~ResultDialog()
    {
        writeConfig();
    }

    void readConfig()
    {
        KConfigGroup dialog(KSharedConfig::openStateConfig(), "ResultDialog");
        const QSize size = dialog.readEntry("Size", QSize(600, 400));
        if (size.isValid()) {
            resize(size);
        }
    }
    void writeConfig()
    {
        KConfigGroup dialog(KSharedConfig::openStateConfig(), "ResultDialog");
        dialog.writeEntry("Size", size());
        dialog.sync();
    }

private:
    QLabel createdLB;
    QListWidget createdLW;
    QLabel errorsLB;
    QListWidget errorsLW;
    QDialogButtonBox buttonBox;
    QVBoxLayout vlay;
};

}

#ifdef Q_OS_UNIX
static const bool HAVE_UNIX = true;
#else
static const bool HAVE_UNIX = false;
#endif

static const Qt::CaseSensitivity fs_cs = HAVE_UNIX ? Qt::CaseSensitive : Qt::CaseInsensitive; // can we use QAbstractFileEngine::caseSensitive()?

static QStringList fs_sort(QStringList l)
{
    std::sort(l.begin(), l.end(), [](const QString &lhs, const QString &rhs) {
                                    return QString::compare(lhs, rhs, fs_cs) < 0;
                                  });
    return l;
}

static QStringList fs_intersect(QStringList l1, QStringList l2)
{
    fs_sort(l1);
    fs_sort(l2);
    QStringList result;
    std::set_intersection(l1.begin(), l1.end(),
                          l2.begin(), l2.end(),
                          std::back_inserter(result),
                          [](const QString &lhs, const QString &rhs) {
                              return QString::compare(lhs, rhs, fs_cs) < 0;
                          });
    return result;
}

static QList<QRegExp> get_patterns(const std::vector< std::shared_ptr<ChecksumDefinition> > &checksumDefinitions)
{
    QList<QRegExp> result;
    for (const std::shared_ptr<ChecksumDefinition> &cd : checksumDefinitions)
        if (cd) {
            const auto patterns = cd->patterns();
            for (const QString &pattern : patterns) {
                result.push_back(QRegExp(pattern, fs_cs));
            }
        }
    return result;
}

namespace
{

struct matches_any : std::unary_function<QString, bool> {
    const QList<QRegExp> m_regexps;
    explicit matches_any(const QList<QRegExp> &regexps) : m_regexps(regexps) {}
    bool operator()(const QString &s) const
    {
        return std::any_of(m_regexps.cbegin(), m_regexps.cend(),
                           [s](const QRegExp &rx) { return rx.exactMatch(s); });
    }
};
}

class CreateChecksumsController::Private : public QThread
{
    Q_OBJECT
    friend class ::Kleo::Crypto::CreateChecksumsController;
    CreateChecksumsController *const q;
public:
    explicit Private(CreateChecksumsController *qq);
    ~Private() override;

Q_SIGNALS:
    void progress(int, int, const QString &);

private:
    void slotOperationFinished()
    {
#ifndef QT_NO_PROGRESSDIALOG
        if (progressDialog) {
            progressDialog->setValue(progressDialog->maximum());
            progressDialog->close();
        }
#endif // QT_NO_PROGRESSDIALOG
        auto const dlg = new ResultDialog(created, errors);
        dlg->setAttribute(Qt::WA_DeleteOnClose);
        q->bringToForeground(dlg);
        if (!errors.empty())
            q->setLastError(gpg_error(GPG_ERR_GENERAL),
                            errors.join(QLatin1Char('\n')));
        q->emitDoneOrError();
    }
    void slotProgress(int current, int total, const QString &what)
    {
        qCDebug(KLEOPATRA_LOG) << "progress: " << current << "/" << total << ": " << qPrintable(what);
#ifndef QT_NO_PROGRESSDIALOG
        if (!progressDialog) {
            return;
        }
        progressDialog->setMaximum(total);
        progressDialog->setValue(current);
        progressDialog->setLabelText(what);
#endif // QT_NO_PROGRESSDIALOG
    }

private:
    void run() override;

private:
#ifndef QT_NO_PROGRESSDIALOG
    QPointer<QProgressDialog> progressDialog;
#endif
    mutable QMutex mutex;
    const std::vector< std::shared_ptr<ChecksumDefinition> > checksumDefinitions;
    std::shared_ptr<ChecksumDefinition> checksumDefinition;
    QStringList files;
    QStringList errors, created;
    bool allowAddition;
    volatile bool canceled;
};

CreateChecksumsController::Private::Private(CreateChecksumsController *qq)
    : q(qq),
#ifndef QT_NO_PROGRESSDIALOG
      progressDialog(),
#endif
      mutex(),
      checksumDefinitions(ChecksumDefinition::getChecksumDefinitions()),
      checksumDefinition(ChecksumDefinition::getDefaultChecksumDefinition(checksumDefinitions)),
      files(),
      errors(),
      created(),
      allowAddition(false),
      canceled(false)
{
    connect(this, SIGNAL(progress(int,int,QString)),
            q, SLOT(slotProgress(int,int,QString)));
    connect(this, &Private::progress,
            q, &Controller::progress);
    connect(this, SIGNAL(finished()),
            q, SLOT(slotOperationFinished()));
}

CreateChecksumsController::Private::~Private()
{
    qCDebug(KLEOPATRA_LOG);
}

CreateChecksumsController::CreateChecksumsController(QObject *p)
    : Controller(p), d(new Private(this))
{

}

CreateChecksumsController::CreateChecksumsController(const std::shared_ptr<const ExecutionContext> &ctx, QObject *p)
    : Controller(ctx, p), d(new Private(this))
{

}

CreateChecksumsController::~CreateChecksumsController()
{
    qCDebug(KLEOPATRA_LOG);
}

void CreateChecksumsController::setFiles(const QStringList &files)
{
    kleo_assert(!d->isRunning());
    kleo_assert(!files.empty());
    const QList<QRegExp> patterns = get_patterns(d->checksumDefinitions);
    if (!std::all_of(files.cbegin(), files.cend(), matches_any(patterns)) &&
            !std::none_of(files.cbegin(), files.cend(), matches_any(patterns))) {
        throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Create Checksums: input files must be either all checksum files or all files to be checksummed, not a mixture of both."));
    }
    const QMutexLocker locker(&d->mutex);
    d->files = files;
}

void CreateChecksumsController::setAllowAddition(bool allow)
{
    kleo_assert(!d->isRunning());
    const QMutexLocker locker(&d->mutex);
    d->allowAddition = allow;
}

bool CreateChecksumsController::allowAddition() const
{
    const QMutexLocker locker(&d->mutex);
    return d->allowAddition;
}

void CreateChecksumsController::start()
{

    {
        const QMutexLocker locker(&d->mutex);

#ifndef QT_NO_PROGRESSDIALOG
        d->progressDialog = new QProgressDialog(i18n("Initializing..."), i18n("Cancel"), 0, 0);
        applyWindowID(d->progressDialog);
        d->progressDialog->setAttribute(Qt::WA_DeleteOnClose);
        d->progressDialog->setMinimumDuration(1000);
        d->progressDialog->setWindowTitle(i18nc("@title:window", "Create Checksum Progress"));
        connect(d->progressDialog.data(), &QProgressDialog::canceled, this, &CreateChecksumsController::cancel);
#endif // QT_NO_PROGRESSDIALOG

        d->canceled = false;
        d->errors.clear();
        d->created.clear();
    }

    d->start();

}

void CreateChecksumsController::cancel()
{
    qCDebug(KLEOPATRA_LOG);
    const QMutexLocker locker(&d->mutex);
    d->canceled = true;
}

namespace
{

struct Dir {
    QDir dir;
    QString sumFile;
    QStringList inputFiles;
    quint64 totalSize;
    std::shared_ptr<ChecksumDefinition> checksumDefinition;
};

}

static QStringList remove_checksum_files(QStringList l, const QList<QRegExp> &rxs)
{
    QStringList::iterator end = l.end();
    for (const QRegExp &rx : rxs) {
        end = std::remove_if(l.begin(), end,
                             [rx](const QString &str) { 
                                return rx.exactMatch(str);
                             });
    }
    l.erase(end, l.end());
    return l;
}

namespace
{
struct File {
    QString name;
    QByteArray checksum;
    bool binary;
};
}

static QString decode(const QString &encoded)
{
    QString decoded;
    decoded.reserve(encoded.size());
    bool shift = false;
    for (QChar ch : encoded)
        if (shift) {
            switch (ch.toLatin1()) {
            case '\\': decoded += QLatin1Char('\\'); break;
            case 'n':  decoded += QLatin1Char('\n'); break;
            default:
                qCDebug(KLEOPATRA_LOG) << "invalid escape sequence" << '\\' << ch << "(interpreted as '" << ch << "')";
                decoded += ch;
                break;
            }
            shift = false;
        } else {
            if (ch == QLatin1Char('\\')) {
                shift = true;
            } else {
                decoded += ch;
            }
        }
    return decoded;
}

static std::vector<File> parse_sum_file(const QString &fileName)
{
    std::vector<File> files;
    QFile f(fileName);
    if (f.open(QIODevice::ReadOnly)) {
        QTextStream s(&f);
        QRegExp rx(QLatin1String("(\\?)([a-f0-9A-F]+) ([ *])([^\n]+)\n*"));
        while (!s.atEnd()) {
            const QString line = s.readLine();
            if (rx.exactMatch(line)) {
                Q_ASSERT(!rx.cap(4).endsWith(QLatin1Char('\n')));
                const File file = {
                    rx.cap(1) == QLatin1String("\\") ? decode(rx.cap(4)) : rx.cap(4),
                    rx.cap(2).toLatin1(),
                    rx.cap(3) == QLatin1String("*"),
                };
                files.push_back(file);
            }
        }
    }
    return files;
}

static quint64 aggregate_size(const QDir &dir, const QStringList &files)
{
    quint64 n = 0;
    for (const QString &file : files) {
        n += QFileInfo(dir.absoluteFilePath(file)).size();
    }
    return n;
}

static std::shared_ptr<ChecksumDefinition> filename2definition(const QString &fileName,
        const std::vector< std::shared_ptr<ChecksumDefinition> > &checksumDefinitions)
{
    for (const std::shared_ptr<ChecksumDefinition> &cd : checksumDefinitions) {
        if (cd) {
            const auto patterns = cd->patterns();
            for (const QString &pattern : patterns) {
                if (QRegExp(pattern, fs_cs).exactMatch(fileName)) {
                    return cd;
                }
            }
        }
    }
    return std::shared_ptr<ChecksumDefinition>();
}

static std::vector<Dir> find_dirs_by_sum_files(const QStringList &files, bool allowAddition,
        const std::function<void(int)> &progress,
        const std::vector< std::shared_ptr<ChecksumDefinition> > &checksumDefinitions)
{

    const QList<QRegExp> patterns = get_patterns(checksumDefinitions);

    std::vector<Dir> dirs;
    dirs.reserve(files.size());

    int i = 0;

    for (const QString &file : files) {

        const QFileInfo fi(file);
        const QDir dir = fi.dir();
        const QStringList entries = remove_checksum_files(dir.entryList(QDir::Files), patterns);

        QStringList inputFiles;
        if (allowAddition) {
            inputFiles = entries;
        } else {
            const std::vector<File> parsed = parse_sum_file(fi.absoluteFilePath());
            QStringList oldInputFiles;
            oldInputFiles.reserve(parsed.size());
            std::transform(parsed.cbegin(), parsed.cend(), std::back_inserter(oldInputFiles),
                           std::mem_fn(&File::name));
            inputFiles = fs_intersect(oldInputFiles, entries);
        }

        const Dir item = {
            dir,
            fi.fileName(),
            inputFiles,
            aggregate_size(dir, inputFiles),
            filename2definition(fi.fileName(), checksumDefinitions)
        };

        dirs.push_back(item);

        if (progress) {
            progress(++i);
        }

    }
    return dirs;
}

namespace
{
struct less_dir : std::binary_function<QDir, QDir, bool> {
    bool operator()(const QDir &lhs, const QDir &rhs) const
    {
        return QString::compare(lhs.absolutePath(), rhs.absolutePath(), fs_cs) < 0;
    }
};
}

static std::vector<Dir> find_dirs_by_input_files(const QStringList &files, const std::shared_ptr<ChecksumDefinition> &checksumDefinition, bool allowAddition,
        const std::function<void(int)> &progress,
        const std::vector< std::shared_ptr<ChecksumDefinition> > &checksumDefinitions)
{
    Q_UNUSED(allowAddition)
    if (!checksumDefinition) {
        return std::vector<Dir>();
    }

    const QList<QRegExp> patterns = get_patterns(checksumDefinitions);

    std::map<QDir, QStringList, less_dir> dirs2files;

    // Step 1: sort files by the dir they're contained in:

    std::deque<QString> inputs(files.begin(), files.end());

    int i = 0;
    while (!inputs.empty()) {
        const QString file = inputs.front();
        inputs.pop_front();
        const QFileInfo fi(file);
        if (fi.isDir()) {
            QDir dir(file);
            dirs2files[ dir ] = remove_checksum_files(dir.entryList(QDir::Files), patterns);
            const auto entryList = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
            std::transform(entryList.cbegin(), entryList.cend(),
                           std::inserter(inputs, inputs.begin()),
                           [&dir](const QString &entry) {
                               return dir.absoluteFilePath(entry);
                           });
        } else {
            dirs2files[fi.dir()].push_back(file);
        }
        if (progress) {
            progress(++i);
        }
    }

    // Step 2: convert into vector<Dir>:

    std::vector<Dir> dirs;
    dirs.reserve(dirs2files.size());

    for (auto it = dirs2files.begin(), end = dirs2files.end(); it != end; ++it) {

        const QStringList inputFiles = remove_checksum_files(it->second, patterns);
        if (inputFiles.empty()) {
            continue;
        }

        const Dir dir = {
            it->first,
            checksumDefinition->outputFileName(),
            inputFiles,
            aggregate_size(it->first, inputFiles),
            checksumDefinition
        };
        dirs.push_back(dir);

        if (progress) {
            progress(++i);
        }

    }
    return dirs;
}

static QString process(const Dir &dir, bool *fatal)
{
    const QString absFilePath = dir.dir.absoluteFilePath(dir.sumFile);
    QTemporaryFile out;
    QProcess p;
    if (!out.open()) {
        return QStringLiteral("Failed to open Temporary file.");
    }
    p.setWorkingDirectory(dir.dir.absolutePath());
    p.setStandardOutputFile(out.fileName());
    const QString program = dir.checksumDefinition->createCommand();
    dir.checksumDefinition->startCreateCommand(&p, dir.inputFiles);
    p.waitForFinished();
    qCDebug(KLEOPATRA_LOG) << "[" << &p << "] Exit code " << p.exitCode();

    if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) {
        if (fatal && p.error() == QProcess::FailedToStart) {
            *fatal = true;
        }
        if (p.error() == QProcess::UnknownError)
            return i18n("Error while running %1: %2", program,
                        QString::fromLocal8Bit(p.readAllStandardError().trimmed().constData()));
        else {
            return i18n("Failed to execute %1: %2", program, p.errorString());
        }
    }

    QFileInfo fi(absFilePath);
    if (!(fi.exists() && !QFile::remove(absFilePath)) && QFile::copy(out.fileName(), absFilePath)) {
        return QString();
    }

    return xi18n("Failed to overwrite <filename>%1</filename>.", dir.sumFile);
}

namespace
{
static QDebug operator<<(QDebug s, const Dir &dir)
{
    return s << "Dir(" << dir.dir << "->" << dir.sumFile << "<-(" << dir.totalSize << ')' << dir.inputFiles << ")\n";
}
}

void CreateChecksumsController::Private::run()
{

    QMutexLocker locker(&mutex);

    const QStringList files = this->files;
    const std::vector< std::shared_ptr<ChecksumDefinition> > checksumDefinitions = this->checksumDefinitions;
    const std::shared_ptr<ChecksumDefinition> checksumDefinition = this->checksumDefinition;
    const bool allowAddition = this->allowAddition;

    locker.unlock();

    QStringList errors;
    QStringList created;

    if (!checksumDefinition) {
        errors.push_back(i18n("No checksum programs defined."));
        locker.relock();
        this->errors = errors;
        return;
    } else {
        qCDebug(KLEOPATRA_LOG) << "using checksum-definition" << checksumDefinition->id();
    }

    //
    // Step 1: build a list of work to do (no progress):
    //

    const QString scanning = i18n("Scanning directories...");
    Q_EMIT progress(0, 0, scanning);

    const bool haveSumFiles = std::all_of(files.cbegin(), files.cend(), matches_any(get_patterns(checksumDefinitions)));
    const auto progressCb = [this, &scanning](int c) { Q_EMIT progress(c, 0, scanning); };
    const std::vector<Dir> dirs = haveSumFiles
                                  ? find_dirs_by_sum_files(files, allowAddition, progressCb, checksumDefinitions)
                                  : find_dirs_by_input_files(files, checksumDefinition, allowAddition, progressCb, checksumDefinitions);

    for (const Dir &dir : dirs) {
        qCDebug(KLEOPATRA_LOG) << dir;
    }

    if (!canceled) {

        Q_EMIT progress(0, 0, i18n("Calculating total size..."));

        const quint64 total = kdtools::accumulate_transform(dirs.cbegin(), dirs.cend(),
                                                            std::mem_fn(&Dir::totalSize),
                                                            Q_UINT64_C(0));

        if (!canceled) {

            //
            // Step 2: perform work (with progress reporting):
            //

            // re-scale 'total' to fit into ints (wish QProgressDialog would use quint64...)
            const quint64 factor = total / std::numeric_limits<int>::max() + 1;

            quint64 done = 0;
            for (const Dir &dir : dirs) {
                Q_EMIT progress(done / factor, total / factor,
                                i18n("Checksumming (%2) in %1", dir.checksumDefinition->label(), dir.dir.path()));
                bool fatal = false;
                const QString error = process(dir, &fatal);
                if (!error.isEmpty()) {
                    errors.push_back(error);
                } else {
                    created.push_back(dir.dir.absoluteFilePath(dir.sumFile));
                }
                done += dir.totalSize;
                if (fatal || canceled) {
                    break;
                }
            }
            Q_EMIT progress(done / factor, total / factor, i18n("Done."));

        }
    }

    locker.relock();

    this->errors = errors;
    this->created = created;

    // mutex unlocked by QMutexLocker

}

#include "moc_createchecksumscontroller.cpp"
#include "createchecksumscontroller.moc"
